package edu.hawaii.duedates;

// The reference libraries coupled to DueDates.
import argparser.ArgParser;
import argparser.BooleanHolder;
import argparser.DoubleHolder;
import argparser.IntHolder;
import argparser.StringHolder;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import org.xml.sax.SAXParseException;
import edu.hawaii.duedates.library.Collection;
import edu.hawaii.duedates.library.LibraryType;
import edu.hawaii.duedates.messenger.Messenger;
import edu.hawaii.duedates.messenger.IMessenger.MessageType;
import edu.hawaii.duedates.user.User;
import edu.hawaii.duedates.userlibraryinfo.UserLibraryInfo;


/**
 * The DueDates class accesses a given library for a given user to acquire the checked out items
 * information.  DueDates receives arguments from the command line that include the library to 
 * request information from, and the user id and the password belonging to required to log in to
 * the library's public web site.
 * 
 * DueDates works with the University of Hawaii and Hawaii State Library systems.
 * 
 * @author Ronn Reeves and Robin E. Raqueno
 */
public final class DueDates {

  /** Console flag parameter. */
  BooleanHolder console = new BooleanHolder(Boolean.FALSE);
  
  /** Email flag parameter. */
  boolean email = Boolean.FALSE;
  
  /** Email account parameter. */
  String emailAccount = "";
  
  /** Email parameters. */
  String[] emailParams = new String[2];
  
  /** Wakeup interval of running the program parameter. */
  DoubleHolder interval = new DoubleHolder(0);

  /** Library collection. */
  List<LibraryType> libraries;

  /** Collection of unmatched command line arguments. */
  //private static String[] unmatchedArgs;
  List<String> librariesAndUsersArgs;

  /** Library parameter count. */
  int libraryCount = 0;

  /** Amount of milliseconds in a day. */
  private static final int MILLISECOND_DAY = 1000 * 60 * 60 * 24;

  /** The output message. */
  private final Messenger msg;

  /** Package location. */
  static final String packagePath = "edu.hawaii.duedates.library";

  /** Repository parameter. */
  StringHolder repository = new StringHolder("libraries.xml");

  /** SMTP server parameter. */
  String smtpServer = "";

  /** Sort option parameter. */
  StringHolder sortOption = new StringHolder("library");

  /** The user library pairs to be queried. */
  UserLibraryInfo uli;

  /** Usage example message. */
  static final String usageMessage = 
    "java -jar DueDates.jar <library code> <user id> <last name> "
    + "[-console] "
    + "[-email <user@domain.ext> <mail.domain.ext>] "
    + "[-repository <filename.xml>] "
    + "[-sort <library | duedate>] "
    + "[-verbose] " 
    + "[-within <days>] " 
    + "[-wakeup <interval>] "
    + "\n";

  /** Email Parameters validity flag. */
  boolean validEmailParameters = Boolean.FALSE;
  
  /** XML file validity flag. */
  boolean validRepository = Boolean.TRUE;

  /** Verbose flag parameter. */
  BooleanHolder verbose = new BooleanHolder(Boolean.FALSE);

  /** Within days of being due parameter. */
  IntHolder withinDays = new IntHolder(-1);

  /**
   * Displays the requested checked out items information for the libraries and users passed to
   * DueDates from the command line.
   * 
   * @param args  The command line arguments passed to the program by the user.
   */
  DueDates(String[] args) {
    msg = new Messenger();
    
    // Load the command repository and parse command line arguments.
    createCommandListing(args);
    
    // Complete request if valid output destination specified.
    if (validEmailParameters || this.console.value) {
      // Load the library repository
      importLibraries();

      getResults(args);
    }
    
    else {
      this.report();
    }
  }
  
  
  /**
   * Parse the command line arguments for matched and unmatched values to allowed command line 
   * options.
   * 
   * @param args The command line arguments.
   */
  void createCommandListing(String[] args) {
    ArgParser parser = new ArgParser(usageMessage);
      
    try {
      // Create command line options.
      parser.addOption("-console %v #Enable console output. (Default)\n", this.console);

      parser.addOption("-email %sX2 #[username@domain.ext] [mail.domain.ext]"
        + "#Email account and SMTP server with optional port number.", this.emailParams);
      
      parser.addOption("-repository %s #[filename.xml]#Library repository XML file.\n", 
        this.repository);
      
      parser.addOption("-sort %s {library, duedate}#[library | duedate]" +
        "#Sort by library or due date.\n", this.sortOption);
      
      parser.addOption("-verbose %v #Enable display of debugging info.\n", this.verbose);
      msg.setVerbose(this.verbose.value);
      
      parser.addOption("-within %d {[0, 999]}#[Within Day(s)]" +
        "#Number of days until book due date\n", this.withinDays);
      
      parser.addOption("-wakeup %f #[Interval]" +
        "#Number of times the program should run\n", interval);

      // Unmatched arguments from command line that should be library and user credential settings.
      String[] unmatchedArgs = parser.matchAllArgs(args, 0, ArgParser.EXIT_ON_ERROR);

      // Set email parameters if required.
      validEmailParameters = setEmailParameters();
                  
      // Copy unmatched arguments in to an array list.
      librariesAndUsersArgs = new ArrayList<String>();
      boolean hasUnmatchedArgs = (unmatchedArgs.length != 0);
      if (hasUnmatchedArgs) {
        for (String s : unmatchedArgs) {
          librariesAndUsersArgs.add(s);
          if (s.startsWith("-")) {
            libraryCount++;
          }
        }
      }

      // Verify the repository is an xml file.
      boolean isInvalidRepository = !(repository.value.endsWith(".xml"));
      if (isInvalidRepository) {
        validRepository = Boolean.FALSE;
        msg.appendStackTrace(new SAXParseException("", null));
      }
      
      wakeUpArgs(args);
    }

    catch (Exception e) {
      if (libraryCount == 0) {
        validRepository = Boolean.FALSE;
        msg.append(MessageType.MISSING_QUERY_PARAMETERS);
      }

      msg.append(MessageType.HELP, "\n" + parser.getHelpMessage());
      
      msg.appendStackTrace(e);
    }
  }

    
  /**
   * Parses the unmatchedArgs to get the libraries to query from the library repository, and
   * the users for whom to query for.
   *  
   * @param libraries The library repository.
   * @param args The libraries passed from the command line.
   * 
   * @return UserLibraryInfo The UserLibraryInfo object holding the user library pairs.
   */
  UserLibraryInfo createUserLibraryPairs(List<LibraryType> libraries,
    List<String> args) {
    UserLibraryInfo uli = new UserLibraryInfo();

    try {
      boolean hasLibraries = (libraryCount > 0);
      boolean hasUsers = !(args.isEmpty());
      
      // Either process libraries provided from command line by user,
      if (hasLibraries) {
        // Processes usage: -library_code user_id password
        for (int index = 0; index < args.size(); index++) {
          for (LibraryType library : libraries) {
            if (library.getCode().equals(args.get(index))) {
              // If the index + 2 is less than the size of the arguments, and neither of the 
              // two following arguments begins with a dash, the user credentials have been 
              // provided.
              if (((index + 2) < args.size()) 
                && !(args.get(index + 1).startsWith("-")) 
                && !(args.get(index + 2).startsWith("-"))) {
                uli.add(new User(args.get(++index), args.get(++index)), library);
              }

              // Otherwise, the number of credentials for this user have not been provided.
              else {
                reportUserCredentialError(library);
              }
            }
          }
        }
      }

      // ...Or only user credentials were passed from command line and are to be assigned
      // to the libraries as ordered in the library repository data file,
      else if (hasUsers) {
        // Processes usage: user_id password (when no -library_code is provided)
        int index = 0;
        while (index < args.size()) {
          for (LibraryType library : libraries) {
            // If the index + 1 is less than the size of the arguments, the user credentials 
            // have been provided.
            if ((index + 1) < args.size()) {
              User usr = new User(args.get(index), args.get(index + 1));
              uli.add(usr, library);
            }

            // Otherwise, the number of credentials for this user have not been provided.
            else {
              reportUserCredentialError(library);
            }
            
            // Increment the index by 2 to move next set of user credentials.
            index += 2;
            
            // All user credentials have been processed and libraries remain, then do not
            // attempt to add more libraries.
            if (index == args.size()) {
              break;
            }
          }
          index = args.size();
        }
      }

      // ...Or no library and user credentials entered on the command line.
      else {
        msg.append(MessageType.SIMPLE, "No libraries or user credentials entered.");
      }
    }
    
    catch (NullPointerException e) {
      msg.appendStackTrace(e);
    }

    return uli;
  }
  
  
  /**
   * Gets the user library pair results.
   * 
   * @param args  The command line arguments passed to the program by the user.
   */
  void getResults(String[] args) {
    if (validRepository) {
      // Create a collection of user-library info objects.
      uli = createUserLibraryPairs(libraries, librariesAndUsersArgs);

      boolean hasUsers = !(uli.getUsers().size() == 0);
      if (hasUsers) {
        // Request checked out items at given interval.
        wakeUp(args, interval.value);
      }
    }
  }
  
  
  /**
   * Creates the collection of libraries.
   */
  void importLibraries() {
    // The repository of libraries.
    List<LibraryType> libraryList = null;

    // Attempt to import the library repository.
    try {
      // Unmarshall the data from the repository into a library collection.
      JAXBContext jc = JAXBContext.newInstance(packagePath);
      Unmarshaller unmarshaller = jc.createUnmarshaller();
      Collection collection = (Collection) unmarshaller.unmarshal(new File(repository.value));
      Collection.Libraries libraries = collection.getLibraries();
      libraryList = libraries.getLibrary();
    }
    
    catch (UnmarshalException e) {
      msg.appendStackTrace(e);
    }

    // Report issues to the console
    catch (Exception e) {
      msg.appendStackTrace(e);
    }

    // Set the library collection.
    libraries = libraryList;   
  }
  
  
  /**
   * Executes an instance of the DueDates class.
   * 
   * @param args  The command line arguments passed to the program by the user.
   */
  public static void main(String[] args) {
    new DueDates(args);
  }


  /**
   * Outputs message indicating the supplied user credentials were insufficient for this library.
   * 
   * @param library The library without valid user credentials being passed.
   */
  void reportUserCredentialError(LibraryType library) {
    msg.append(MessageType.SIMPLE, "\n>>> INVALID USER CREDENTIALS FOR " 
      + library.getName().toUpperCase() + ".  ");
    msg.append(MessageType.SIMPLE, "User credentials must consist of a user id and a password.");
    msg.append(MessageType.SIMPLE, "\n>>> THIS LIBRARY WILL NOT BE QUERIED.");
  }

  
  /**
   * Requests info from the libraries.
   */
  void requestData() {
    // Request library information.
    msg.append(MessageType.REQUEST_DATA);
    uli.requestInfo();

    // Display sorted books within the given due date period.
    msg.append(MessageType.SIMPLE, uli.createReport(sortOption.value, withinDays.value));
  }
  
  
  /**
   * Reports the output of this query.
   */
  void report() {
    if (email) {
      msg.report(this.emailAccount, this.smtpServer);
    }
    
    else {
      msg.report();
    }
  }


  /**
   * Sets email parameters.
   * 
   * @return Returns true if set, otherwise false.
   */
  boolean setEmailParameters() {
    // Set default output to console if no email parameters passed.
    if (this.emailParams[0] == null) {
      this.console.value = Boolean.TRUE;
      return Boolean.FALSE;
    }
    
    // Pattern for email addresses of the form:  username@domain.ext
    Pattern emailFormat = Pattern.compile("\\S+@\\S+\\.\\S+");
    
    // Get user account parameter.
    this.emailAccount = this.emailParams[0];
    
    // Verify email account parameter is valid email address.
    Matcher matchEmailAddress = emailFormat.matcher(this.emailAccount);
    boolean isValidEmailAddress = Boolean.FALSE;
    if (matchEmailAddress.matches()) {
      isValidEmailAddress = Boolean.TRUE;
    }

    // Pattern for SMTP Server of the form:  mail.domain.ext
    Pattern smtpServerFormat = Pattern.compile("(\\S+\\.){2}\\S+$");

    // Get SMTP server parameter.
    this.smtpServer = this.emailParams[1];

    // Verify SMTP Server parameter is valid SMTP Server.
    Matcher matchSmtpServer = smtpServerFormat.matcher(this.smtpServer);
    boolean isValidSmtpServer = Boolean.FALSE;
    if (matchSmtpServer.matches()) {
      isValidSmtpServer = Boolean.TRUE;
    }

    // Set email validity.
    this.email = (isValidEmailAddress && isValidSmtpServer);
    
    // Set console as default output, if email is not selected.
    if (this.email) {
      this.console.value = Boolean.FALSE;
      return Boolean.TRUE;
    }

    boolean invalidEmailAddress = !isValidEmailAddress;
    if (invalidEmailAddress) {
      msg.append(MessageType.INVALID_EMAIL_PARAMETER, this.emailAccount);
    }
    
    boolean invalidSmtpServer = !isValidSmtpServer;
    if (invalidSmtpServer) {
      msg.append(MessageType.INVALID_EMAIL_PARAMETER, this.smtpServer);
    }

    return Boolean.FALSE;
  }
  
  
  /**
   * Outputs due dates to a string.
   * 
   * @return A string representing this DueDates instance.
   */
  public String toString() {
    return usageMessage;
  }


  /**
   * Queries the libraries at a given interval of time.
   * 
   * @param args  The new set of arguments.
   * @param interval  The interval period for querying data.
   */
  public void wakeUp(String[] args, double interval) {
    if ( interval == 0) {
      this.requestData();
      this.report();
    }
    
    else {
      String[] timerArgs = wakeUpArgs(args);
      Calendar cal = Calendar.getInstance(Locale.US);
      int val = (int) (interval * MILLISECOND_DAY);
      DueDatesTimerTask dt = new DueDatesTimerTask(timerArgs);
      Timer timer = new Timer();
      timer.schedule(dt, cal.getTime(), val);
    } 
  }
  
  
  /**
   * This will stored the arguments in a new array.
   * 
   * @param args  The arguments to be stored
   * @return newArgs The set of new arguments.
   */
  public String[] wakeUpArgs(String[] args) {
    String[] newArgs = new String[args.length];
    
    for (int i = 0; i < args.length; i++) {
      if ("-wakeup".equals(args[i])) {
        args[i + 1] = "0";
      }
      newArgs[i] = args[i];
    }
    
    return newArgs;
  }
}